import os
import string
import random
import codecs
import yaml
from functools import reduce

from flask_restful import Resource, reqparse
import docker
import werkzeug
import dateutil.parser
from jwt.exceptions import PyJWTError

from app.models import Image as ImageDocument
from app.models import Api, Response, Schema, Param
from app.resources.helpers import *
from config import config_object

dir_name = os.path.dirname(__file__)
template_folder = os.path.join(os.path.abspath(dir_name), '..',
                               'dockerfile_templates')
tmp_folder = os.path.join(os.path.abspath(dir_name), '..', 'tmp')
os.makedirs(tmp_folder, exist_ok=True)


class Image(Resource):
    docker_url = config_object.DOCKER_URL
    client = docker.DockerClient(base_url=docker_url) if docker_url \
        else docker.from_env()

    default_from_image_map = {
        'java8': 'openjdk:8-jre-alpine',
        'python3': 'python:3.7-alpine'
    }
    parser = reqparse.RequestParser()
    parser.add_argument('exeFile',
                        type=werkzeug.datastructures.FileStorage,
                        location='files')
    parser.add_argument('apiFile',
                        type=werkzeug.datastructures.FileStorage,
                        location='files')
    parser.add_argument('tag')
    parser.add_argument('language')
    parser.add_argument('description')
    parser.add_argument('Authorization', location='headers')
    parser.add_argument('fromImage')
    time_fmt = '%m/%d/%Y, %H:%M:%S'

    @staticmethod
    def random_string(n: int = 16) -> str:
        return ''.join(random.choices(string.ascii_letters, k=n))

    @staticmethod
    def __trim_image_data(item) -> []:
        tags = item['RepoTags']
        # get created timestamp in isoformat
        # raw_created sample: 2019-11-08T01:09:48.258849908Z
        raw_created = item['Created']
        created = dateutil.parser.parse(raw_created).replace(microsecond=0) \
            .isoformat()
        # get envs
        # raw_envs sample: ['JAVA_VERSION=8u212']
        raw_envs = item['Config']['Env']
        # x is the first params and it's {} when initial
        # **x is the deconstruction of dict,
        envs = reduce(
            lambda x, y: {**x, y[0: y.find('=')]: y[y.find('=') + 1:]},
            raw_envs, {})
        # get last tag time in isoformat
        raw_last_tag_time = item['Metadata']['LastTagTime']
        last_tag_time = dateutil.parser.parse(raw_last_tag_time) \
            .replace(microsecond=0).isoformat()
        # get labels
        labels = item['Config']['Labels']
        # get id
        image_id = item['Id']
        return [{
            'id': image_id,
            'tag': tag,
            'created': created,
            'envs': envs,
            'lastTagTime': last_tag_time,
            'labels': labels
        } for tag in tags]

    @staticmethod
    def __parse_params(params: list) -> []:
        return list(map(lambda param: Param(name=param['name'],
                                            param_in=param['in'],
                                            param_type=param['schema']['type'],
                                            description=param['description']),
                        params))

    @staticmethod
    def __parse_schema(schema: dict) -> Schema:
        if len(schema) == 0:
            return None
        schema_type = schema.get('type', None)
        supported_schema_type = \
            ['number', 'string', 'boolean', 'object', 'array']
        if schema_type not in supported_schema_type:
            print(schema_type)
            raise Exception('schema type not supported')
        if schema_type == 'object':
            # parse properties
            properties = {}
            properties_object = schema.get('properties', {})
            if len(properties_object) == 0:
                raise Exception('schema with object type must contain \
                                properties')
            # not using reduce 'cause python's lambda function sucks.
            for key in properties_object:
                properties[key] = Image.__parse_schema(properties_object[key])
            return Schema(schema_type=schema_type, properties=properties)
        elif schema_type == 'array':
            # parse items
            item_object = schema.get('items', {})
            if len(item_object) == 0:
                raise Exception('schema with array type must contain \
                                items')
            items = Image.__parse_schema(item_object)
            return Schema(schema_type=schema_type, items=items)
        else:
            return Schema(schema_type=schema_type)

    @staticmethod
    def __parse_response(response: dict) -> Response:
        description = response.get('description', None)
        schema_object = response.get('content', {}) \
            .get('application/json', {}) \
            .get('schema', {})
        return Response(description=description,
                        schema=Image.__parse_schema(schema_object))

    @staticmethod
    def __parse_responses(responses: dict) -> []:
        return list(map(Image.__parse_response, responses.values()))

    @staticmethod
    def __parse_api_file(api_file: werkzeug.datastructures.FileStorage) -> []:
        service_object = yaml.load(api_file)
        paths = service_object.get('paths', {})
        api_list = []
        for path in paths:
            path_object = paths[path]
            for method in path_object:
                method_object = path_object[method]
                params = method_object.get('parameters', [])
                responses = method_object.get('responses', [])
                api = Api(name=path,
                          path=path,
                          method=method,
                          description=method_object.get('summary', None),
                          params=Image.__parse_params(params),
                          responses=Image.__parse_responses(responses))
                api_list.append(api)
        return api_list

    # generate image/service document from the parameter of request
    @staticmethod
    def __gen_image_document(image_id, source, name, description, api_file):
        apis = Image.__parse_api_file(api_file)
        return ImageDocument(image_id=image_id,
                             source=source,
                             name=name,
                             description=description,
                             api_list=apis)

    @staticmethod
    def __build_image(exe_file, language, source, tag):
        """
        build docker image from exe_file when using post method
        :type exe_file: file use to build docker image
        """
        from_image = Image.default_from_image_map[language]
        labels = {
            'source': source,
            'language': language,
        }
        jar_file_name = exe_file.filename + Image.random_string()
        exe_file.save(os.path.join(tmp_folder, jar_file_name))
        # generate docker file
        filename = exe_file.filename + Image.random_string()
        with codecs.open(os.path.join(template_folder, language),
                         encoding='utf-8') as template:
            lines = template.readlines()
            new_lines = [
                line.replace('${filename}', jar_file_name).replace(
                    '${from_image}', from_image) for line in lines
            ]
            with codecs.open(os.path.join(tmp_folder, filename),
                             'w') as dst:
                dst.writelines(new_lines)
        context = os.path.abspath(tmp_folder)
        # The first item of returned_tuple is the Image object for the
        #  image that was build, and the second one is a generator of
        #  the build logs as JSON-decoded objects.
        returned_tuple = Image.client.images.build(path=context,
                                                   dockerfile=filename,
                                                   rm=True,
                                                   tag=tag,
                                                   labels=labels,
                                                   )
        # clear temp files
        os.remove(os.path.join(tmp_folder, filename))
        os.remove(os.path.join(tmp_folder, jar_file_name))
        return {
            'image': Image.__trim_image_data(returned_tuple[0].attrs)[0],
            'logs': list(returned_tuple[1])
        }

    def get(self) -> dict:
        data = Image.parser.parse_args()
        try:
            source = get_source_from_data(data)
        except PyJWTError as err:
            return result_error('401', 'invalid jwt token'), 401
        raw_attrs = [
            img.attrs for img in Image.client.images.list(
                filters={'label': f'source={source}'})
        ]
        ret_data = [item for attr in raw_attrs
                    for item in Image.__trim_image_data(attr)]
        # ret_data = list(map(Image.__trim_image_data, raw_attrs))
        return result_success(ret_data)

    # Create a new docker image for later deployment, based on the uploaded
    #  file and the templates in '/dockerfile_templates'. Only jar -> image
    #  is supported for now. Will create labels for the image including the
    #  source and language of it.
    def post(self):
        # parse request
        data = Image.parser.parse_args()
        # the type is werkzeug.datastructures.FileStorage
        exe_file = data['exeFile']
        language = data['language'] or 'java8'
        tag = data['tag']
        description = data['description']
        api_file = data['apiFile']
        try:
            source = get_source_from_data(data)
        except PyJWTError as err:
            return result_error('401', 'invalid jwt token'), 401
        if not tag:
            return result_error('400', 'no tag provided')
        if not exe_file:
            return result_error('400', 'no file found')
        else:
            # build image
            ret_data = Image.__build_image(exe_file, language, source, tag)
            # generate document
            image_id = ret_data['image']['id']
            image_document = Image.__gen_image_document(image_id, source, tag,
                                                        description, api_file)
            # save document to mongo
            image_document.save()
            return result_success(ret_data)

    def delete(self):
        parser = reqparse.RequestParser()
        parser.add_argument('image')
        parser.add_argument('force')
        parser.add_argument('Authorization', location='headers')
        data = parser.parse_args()
        if not data['image']:
            return result_error('400', 'no image specified')
        image_id = data['image']
        try:
            source = get_source_from_data(data)
        except PyJWTError as err:
            return result_error('401', 'invalid jwt token'), 401
        images = Image.client.images
        # before remove
        img_list_bf_rm = images.list()
        image_found = images.get(image_id)
        if image_found.labels.get('source') != source:
            return result_error('500', 'no image found')
        # dont find there was any error will be raised in doc
        try:
            images.remove(image=image_id,
                          force=data['force'] or False)
        except Exception:
            return result_error('428', '无法删除镜像，请检查是否有服务正在使用该镜像！'), 428
        # delete the corresponding data in mongo
        # for better readability
        ret = set(img_list_bf_rm) - set(Image.client.images.list())
        # handel the result if image dont delete successfully
        img_id_list = list(map(lambda img: img.id, ret))
        if image_id in img_id_list:
            ImageDocument.objects(image_id=image_id).delete()
        return result_success(None)
